<?php
/**
 *	1.0
 *	GCM push notification server
 *	本類主要實現單條及多條push信息發送
 *	加入feedback功能，可以對無效的tokens進行標識并更新數據庫
 *	可以插入/更新Device tokens
 *
 *
 *	google push notification server
 *	@author Terry
 *	@version v1.0
 *	@date:	2012-12-04
 */

class Model_MAPI_GCM {	// class start


	protected $send_url = "https://android.googleapis.com/gcm/send";
	protected $_serverApiKey = '';

	protected $debug = false;
	private $_logMaxSize = 1048576;				// 日誌文件最大size
	private $_showErrors = true;				// 是否顯示錯誤信息
	private $_logErrors = true;					// 是否記錄錯誤信息
	private $_logPath = 'logs/push/c2dm.log';	// 日誌路徑

	protected $mode = 'production';	// 'production' or 'sandbox'

	private $_db;

	protected $tableName = 'gcm_devices';
	protected $version = 0;

	private $_message = '';
	private $_messageMaxSize = 800;	// 1024 - 224


	protected function __construct($db, $mode, $tableName='', $version=0){
		$this->_db = $db;
		if($tableName) $this->tableName = $tableName;
		if(isset($version)) $this->version = $version;
		if(isset($mode)) $this->mode=$mode;
	}


	// register to db
	public function registerDevice(Array $param){
		if($this->version==0){
			$this->regDeviceOld($param);
		}else{
			$this->regDeviceNew($param);
		}
    }

	
    // register device old
	private function regDeviceOld(Array $param){
		$appname = '';
		$appversion = '';
		$deviceuid = '';
		$devicetoken = '';
		$devicemodel = '';
		$deviceversion = '';
		$ip = '';

		foreach($param as $key => $val){
			${$key} = $val;
		}

		if($this->_db){
			if(strlen($appname)==0) $this->_triggerError('ERROR: Application Name must not be blank.', E_USER_ERROR);
			
			$sql = "INSERT INTO `".$this->tableName."`
					VALUES (
						NULL,
						'{$appname}',
						'{$appversion}',
						?,
						?,
						'{$devicemodel}',
						'{$deviceversion}',
						'{$this->mode}',
						'active',
						NOW(),
						NOW(),
						0,
						'{$ip}'
					)
					ON DUPLICATE KEY UPDATE
					`appversion`='{$appversion}',
					`devicetoken`=?,
					`devicemodel`='{$devicemodel}',
					`deviceversion`='{$deviceversion}',
					`development`='{$this->mode}',
					`status`='active',
					`modified`=NOW();";
			$a_bind_data = array($deviceuid, $devicetoken, $devicetoken);
			$sth = $this->_db->prepare($sql);
			$sth->execute($a_bind_data);
			$this->debugMsg("Register device token: {$devicetoken}");
		}	
	}


	// register device new
	private function regDeviceNew(Array $param){
    	$app_version = '';
    	$device_id = '';
		$token = '';

		foreach ($param as $key => $val) {
			${$key} = $val;
		}

		if ($this->_db) {
			if(strlen($app_version)==0) $this->_triggerError('ERROR: Application Version must not be blank.', E_USER_ERROR);
			else if(strlen($device_id)==0) $this->_triggerError('ERROR: Device Id must not be blank.', E_USER_ERROR);
			else if(strlen($token)==0) $this->_triggerError('ERROR: Device Token must not be blank.', E_USER_ERROR);
	 	 
			// store device for push notifications
			//$this->_db->query("SET NAMES 'utf8';"); // force utf8 encoding if not your default
			$sql = "INSERT INTO `".$this->tableName."`
					VALUES (
						NULL,
						'{$app_version}',
						'{$device_id}',
						'{$token}',
						'active',
						NOW(),
						NOW()
					)
					ON DUPLICATE KEY UPDATE
					`app_version`='{$app_version}',
					`token`='{$token}',
					`last_status`='active',
					`lastmodify`=NOW();";
			$this->_db->exec($sql);
			$this->debugMsg("Register device token: {$token}");
		}
	}



	// update unregister
	public function unregisterDevice($token, $status='uninstalled'){
		if($this->version==0){
			$this->unregisterDeviceOld($token, $status);
		}else{
			$this->unregisterDeviceNew($token, $status);
		}
	}


	// unregister device old
	private function unregisterDeviceOld($token, $status='uninstalled'){
		if($this->_db){
			if(is_array($token)){	// if token is Arry, token=pid array
				$token = implode(",", $token);
				$sql = "UPDATE `".$this->tableName."`
						SET `status`='".$status."', `modified`=NOW()
						WHERE `pid` in ({$token});";
			}else{
				$sql = "UPDATE `".$this->tableName."`
						SET `status`='".$status."', `modified`=NOW()
						WHERE `devicetoken`='{$token}'
						LIMIT 1;";
			}
			$this->_db->exec($sql);
			$this->debugMsg("Unregister device token: {$token}, status: {$status}");
		}
	}


	// unregister device new
	private function unregisterDeviceNew($token, $status='uninstalled'){
		if($this->_db){
			if(is_array($token)){	// if token is Arry, token=id array
				$token = implode(",", $token);
				$sql = "UPDATE `".$this->tableName."`
						SET `last_status`='".$status."', `lastmodify`=NOW()
						WHERE `id` in ({$token});";
			}else{
				$sql = "UPDATE `".$this->tableName."`
						SET `last_status`='".$status."', `lastmodify`=NOW()
						WHERE `token`='{$token}'
						LIMIT 1;";
			}
			$this->_db->exec($sql);
			$this->debugMsg("Unregister device token: {$token}, status: {$status}");
		}
	}

	
	// push message
	public function pushMessage($token, $data=''){
		if($data=='' && $this->_message==''){
			$this->_triggerError('ERROR: message data is empty .', E_USER_ERROR);
		}else{
			if($data!=''){
				$this->setMessage($data);
			}
		}

		if($this->get_bytes(json_encode($this->_message)) > $this->_messageMaxSize){
			$this->_triggerError('ERROR: The message size limit > 1024 bytes .', E_USER_ERROR);
		}
	
		$header = array();
		$header['Content-Type'] = 'application/json;charset=utf-8';
		$header['Authorization'] = ' key='.$this->_serverApiKey;

		$postFields = array(
				'registration_ids'=> array($token),
				'data'=>$this->_message
		);

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $this->send_url);
		curl_setopt($ch, CURLOPT_HTTPHEADER, $this->setHeader($header));
		curl_setopt($ch, CURLOPT_POST, 1);
		curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postFields));
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		$response = curl_exec($ch);
		$info = curl_getinfo($ch);
		curl_close($ch);

		list($unreg, $retdata) = $this->feedback($info['http_code'], $response);
		if($unreg){
			$this->unregisterDevice($token);
		}

		$this->updatePushDate($token);

		return $retdata;
	}


	// push Enhanced message
	public function batchPushMessage(Array $result, $data='', $checkError=true){
		if($data=='' && $this->_message==''){
			$this->_triggerError('ERROR: message data is empty .', E_USER_ERROR);
		}else{
			$this->setMessage($data);
		}

		if($this->get_bytes(json_encode($this->_message)) > $this->_messageMaxSize){
			$this->_triggerError('ERROR: The message size limit > 1024 bytes .', E_USER_ERROR);
		}

		$unreg_pid = array();
		$fail_device = array();
		$send_pid = array();

		$header = array();
		$header['Content-Type'] = 'application/json;charset=utf-8';
		$header['Authorization'] = ' key='.$this->_serverApiKey;
	
		if($result){
			foreach($result as $device){

				if($this->version==1){	// new version not pid and devicetoken
					$device['pid'] = $device['id'];
					$device['devicetoken'] = $device['token'];
				}

				$postFields = array(
						'registration_ids'=>array($device['devicetoken']),
						'data'=>$this->_message
				);

				$ch = curl_init();
				curl_setopt($ch, CURLOPT_URL, $this->send_url);
				curl_setopt($ch, CURLOPT_HTTPHEADER, $this->setHeader($header));
				curl_setopt($ch, CURLOPT_POST, 1);
				curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postFields));
				curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
				$response = curl_exec($ch);
				$info = curl_getinfo($ch);
				curl_close($ch);

				list($unreg, $retdata) = $this->feedback($info['http_code'], $response);
				if($unreg==1){
					array_push($unreg_pid, $device['pid']);
				}
				if($retdata['success']==0 && $checkError==true){
					array_push($fail_device, array('pid'=>$device['pid'],'devicetoken'=>$device['devicetoken'],'error'=>$retdata['error']));
				}

				array_push($send_pid, $device['pid']);
			}
		
			if(count($unreg_pid)>0){
				$this->unregisterDevice($unreg_pid);
			}

			$this->updatePushDate($send_pid);

			if(count($fail_device)>0){
				return array('success'=>0, 'error'=> $fail_device);
			}else{
				return array('success'=>1, 'error'=>'');
			}
		
		}
	}


	// feedback
	private function feedback($http_code, $response){
		$retdata = array();
		$unreg = 0;
		if($http_code=='200'){
			$feedback = json_decode($response, true);
			if($feedback['success']==1){
				$retdata['success'] = 1;
				$retdata['error'] = '';
			}elseif($feedback['failure']==1){
				$retdata['success'] = 0;
				$retdata['error'] = $feedback['results'][0]['error'];
				if(in_array($retdata['error'], array('InvalidRegistration', 'NotRegistered', 'MismatchSenderId'))){	// 需要unregister
					$unreg = 1;
				}
			}
		}else{
			$retdata['success'] = 0;
			$retdata['error'] = $response;
		}

		return array($unreg, $retdata);
	}


	// update pushdate
	private function updatePushDate($token){
		if($this->version==0){
			$this->updatePushDateOld($token);
		}else{
			$this->updatePushDateNew($token);
		}
	}


	// update pushdate old
	private function updatePushDateOld($token){
		$pushdate = date('Y-m-d H:i:s');
		if(is_array($token)){	// if token is Arry, token=pid array
			$token = implode(",", $token);
			$sql = "UPDATE `".$this->tableName."`
					SET `pushdate`='".$pushdate."'
					WHERE `pid` in ({$token});";
		}else{
			$sql = "UPDATE `".$this->tableName."`
					SET `pushdate`='".$pushdate."'
					WHERE `devicetoken`='{$token}'
					LIMIT 1;";
		}
		$this->_db->exec($sql);
	}


	// update pushdate new
	private function updatePushDateNew($token){

	}


	public function setMode($mode){
		$this->mode = $mode;
	}
	

	public function setTableName($name){
		$this->tableName = (string)$name;
	}


	public function setMessage($msg){
		$this->_message = '';
		$message = array();
		$tmpmsg = array();
		if(is_array($msg)){
			if(!isset($msg['message'])){
				$this->_triggerError('ERROR: "message" key not found .', E_USER_ERROR);
			}
			$tmpmsg = $msg;
		}else{
			$tmpmsg['message'] = $msg;
		}

		foreach($tmpmsg as $key=>$val){
			$message['data.'.$key] = $val;
		}		
		$this->_message = $message;
	}


	public function setSender($sender='', $password='', $service='', $source=''){
		$this->_sender = $sender;
		$this->_password = $password;
		$this->_service = $service;
		$this->_source = $source;
	}


	protected function debugMsg($msg, $flag="Debug") {
		$time = date("Y-m-d H:i:s");
		if ($this->debug) {
			echo "[{$time}][{$flag}] " . $msg . PHP_EOL;
		}
	}


	public function checklimit($message){
		if(!$message){
			$this->setMessage($message);
			if($this->get_bytes(json_encode($this->_message)) > $this->_messageMaxSize){
				return false;
			}
		}
		return true;
	}


	private function get_bytes($str, $from_encoding = null) {
		$from_encoding && $str = mb_convert_encoding($str, 'UTF-8', $from_encoding);
		return strlen($str);
	}


    /**
     * Trigger Error
     *
     * Use PHP error handling to trigger User Errors or Notices.  If logging is enabled, errors will be written to the log as well.
     * Disable on screen errors by setting showErrors to false;
     *
     * @param string $error Error String
     * @param int $type Type of Error to Trigger
     * @access private
	 */

	private function _triggerError($error, $type=E_USER_NOTICE) {

		$datetime = date('Y-m-d H:i:s');
		$error = "[".$datetime."] " . $error;

		$backtrace = debug_backtrace();
        $backtrace = array_reverse($backtrace);
        $error .= "\n";
        $i=1;
		
		foreach($backtrace as $errorcode){
            $file = ($errorcode['file']!='') ? "-> File: ".basename($errorcode['file'])." (line ".$errorcode['line'].")":"";
            $error .= "\n\t".$i.") ".$errorcode['class']."::".$errorcode['function']." {$file}";
            $i++;
        }
		
		$error .= "\n\n";


		if($this->_logErrors && file_exists($this->_logPath)){

			if(filesize($this->_logPath) > $this->_logMaxSize) $fh = fopen($this->_logPath, 'w');
            else $fh = fopen($this->_logPath, 'a');
            fwrite($fh, $error);
            fclose($fh);
        }
		
		if($this->_showErrors) trigger_error($error, $type);
	}


	// create header array
	private function setHeader($header){
		$data = array();
		foreach($header as $key=>$val){
			array_push($data, $key.':'.$val);
		}
		return $data;
	}


}	// class end
?>